# Курсовая рабта: файловая система в ядре ОС

Данные материалы содержат только информацию, касающуюся OpenBSD, FreeBSD и Linux.
При желании студент может осуществлять разработку под любую ОС, но в этом случае
придётся осуществлять заметно больше самостоятельных действий по подготовке.

## OpenBSD и FreeBSD

N.B.: Поддержка FreeBSD пока что не завершена.

Код курсовой работы должен быть реализован как файловая система **cwfs**.
После выполнения инструкций ниже «рыба» будет подготовлена в подкаталоге `cwfs`
каталога с исходными текстами ядра (`/usr/src/sys` по умолчанию в OpenBSD и
`/usr/src/sys/fs` в FreeBSD). В случае, если к имеющимся модулям (`cwfs_vnops.c`
и `cwfs_vfsops.c`) потребуется добавить ещё какие-то, то в файле `conf/files`
в каталоге с исходными текстами ядра потребуется добавить соответствующие строчки.
Учтите, что при этом команда `make unpatch` более не будет работать, а в случае
полной очистки (`make distclean`) все ваши изменения в этом файле пропадут вместе
с самим файлом (впрочем, как и остальные ваши рабочие файлы).

Сама «рыба» берётся из каталога `skel` папки. Изменять код в папке `skel` не
следует, так как это сломает последующие обновления данного репозитория.
Редактировать следует именно файлы, размещённые в дереве исходных текстов ОС.

При сборке в OpenBSD исходные тексты разделены на два компонента:

* `kernel` — собственно ядро ОС
* `base` — базовая система (утилиты, библиотеки, документация и т.д.)

При сборке во FreeBSD компонент всего один — `base`, который включает
в себя всё сразу.

Цели, используемые для выполнения сборки и установки собранных компонентов:

* `make fetch` — скачивает немодифицированные исходные тексты OpenBSD 6.8.
* `make extract` — распаковывает исходные тексты в `BASE_SRC_DIR` и `KERNEL_SRC_DIR`.
* `make skel` — размещает «рыбу» для выполнения курсовой работы.
* `make patch` — применяет патчи, подключающие код курсовой работы к коду системы.
* `make configure` — подготавливает к сборке.
* `make all` (по умолчанию) — сборка всех затронутых компонентов, с патчами.
* `make install` — устанавливает ядро и модифицированные узлы базовой ОС.
* `make `*компонент* — то же, что `make patch-`*компонент*` build-`*компонент*.

При необходимости цели вызывают друг друга. Например, если исходники базовой системы
ещё не были скачаны, то `extract-base` вызовет сначала `fetch-base`.

Дополнительные цели, полезные в работе:

* `make build` — собирает базовую систему и ядро, безотносительно наложения патчей.
* `make clean` — удаляет промежуточные и конечные результаты сборки.
* `make distclean` — удаляет всё, кроме скачанных архивов с исходными текстами.
* `make unpatch` — убирает патчи, подключающие код курсовой работы к коду системы.
   Данная команда не затрагивает файлы

Все цели, кроме `base` и `kernel` версии с суффиксами
для работы только в отношении ядра или базовой ОС, например:
`fetch-kernel`, `install-base` и так далее.

Повторный запуск целей `fetch`, `extract`, `skel`, `patch` и `configure` не приведёт
к повторному выполнению команд, если до этого такая команда завершилась успешно.
Если нужно принудительно, например, заново распаковать исходные тексты,
то можно использовать цель с префиксом `re`:

	make reextract
	make reconfigure
	# и т.д.

### Дополнительная информация

* [OpenBSD vnode(9)](https://man.openbsd.org/vnode.9) — документация по VFS в OpenBSD, со ссылками на связанные компоненты.  В FreeBSD есть аналогичная [страница руководства](https://www.freebsd.org/cgi/man.cgi?query=vnode&sektion=9), однако она менее подробная и полезна больше как источник ссылок на другие страницы.
* [OpenBSD FAQ — Building the System from Source](https://www.openbsd.org/faq/faq5.html) — указания по сборке OpenBSD из исходных текстов в целом.
* [FreeBSD Handbook — Configuring the FreeBSD Kernel](https://docs.freebsd.org/en/books/handbook/kernelconfig/) — указания по сборке ядра FreeBSD.
* [FreeBSD build(7)](https://www.freebsd.org/cgi/man.cgi?query=build&sektion=7) — указания по сборке базовой системы FreeBSD.

## Linux

Подробное описание процесса создания файловой системы уровня ядра в Linux, на английском языке:
[часть 1](https://linux-kernel-labs.github.io/refs/heads/master/labs/filesystems_part1.html),
[часть 2](https://linux-kernel-labs.github.io/refs/heads/master/labs/filesystems_part2.html).

Потребуются исходные тексты ядра; желательно использовать поставляемый
с вашим дистрибутивом Linux пакет исходных текстов, обычно это что-то вроде
`linux-sources` или `kernel-source`. Инструкции по сборке ядра в различных
дистрибутивах Linux приведены ниже:

* [ALT Linux](https://wiki.gentoo.org/wiki/Kernel/Configuration/ru)
* [Arch Linux](https://wiki.archlinux.org/title/Kernel_%28%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9%29/Arch_Build_System_%28%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9%29)
* [CentOS](https://wiki.centos.org/HowTos/Custom_Kernel) (на английском)
* [Debian](https://wiki.debian.org/BuildADebianKernelPackage) (на английском)
* [Gentoo](https://wiki.gentoo.org/wiki/Kernel/Configuration/ru)
* [Ubuntu](https://wiki.ubuntu.com/Kernel/BuildYourOwnKernel) (на английском)

## Общие указания

### Изменение ФС по таймеру

Прежде всего следует выделить действия, которые потребуется выполнять спустя какое-то время (один раз или регулярно — не принципиально) и закодировать их в отдельную функцию (функции). Затем остаётся сказать ядру, что «через столько-то времени выполнить запустить такую-то функцию». Источники информации:

* **OpenBSD**: [timeout(9)](https://man.openbsd.org/timeout_set.9)
* **FreeBSD**: [timeout(9)](https://www.freebsd.org/cgi/man.cgi?query=timeout&sektion=9)
* **Linux**: [Linux Device Drivers — Chapter 7. Time, Delays, and Deferred Work](https://www.oreilly.com/library/view/linux-device-drivers/0596005903/ch07.html)

При этом напрямую из функции, которая будет вызвана по таймеру, обращаться к файловой системе нельзя. Дело в том, что в *BSD, Linux и многих других ОС код ядра работает в двух условных контекстах: контекст обработки прерывания (interrupt context) и контекст процесса (process context). Код в контексте прерываний запускается собственно при обработке прерываний. Таймеры по определению срабатывают по прерыванию.

Фундаментальное различие между контекстами прерывания и контекстом процесса состоит в том, что в контексте процесса допустимо приостановить выполнение, например, в ожидании освобождения блокировки, в то время как в контексте прерывания это невозможно. При доступе к файловой системе необходимость «встать на паузу» возникает часто. Например, два процесса одновременно создают файлы в одном и том же каталоге. Это требует использования блокировок для обеспечения целостности данных файловой системы и, как результат, один из процессов будет приостановлен до освобождения заблокированного ресурса другим процессом. Поэтому все манипуляции с файловой системой должны происходить только в контексте процесса.

Стоит отметить, что в Linux дополнительно ещё выделяют atomic context: это ещё более ограниченный interrupt context, в который код попадает, успешно получив доступ к spinlock.

Чтобы перейти от контекста прерывания к контексту процесса, нужно использовать механизм планирования задач. Задачей будет являться запуск (ещё одной) вашей функции, которая будет работать уже в контексте процесса, выполняя собственно запланированные вами действия в файловой системе. При этом важно помнить, что в этот момент выполнение вашего кода не происходит в контексте того или иного процесса, поэтому, например, никаких сведений о текущем пользователе у вас не будет — если вы «создадите» файл, то владельца, группу и режим доступа вам придётся откуда-то брать, или же жёстко задавать значения (скажем, идентификаторы владельца и группы выставлять в 0).

Таким образом, сначала программируется таймер на запуск функции-обёртки, которая в свою очередь ставит в план запуска фактически требуемую задачу: скажем, появление новых файлов и каталогов.

Чтобы избежать ситуации, когда с вашей ФС попытаются одновременно обращаться и из VFS, и из вашей запланированной задачи, потребуется блокировка, хотя бы банальный мьютекс:

* **OpenBSD**: [mutex(9)](https://man.openbsd.org/mutex.9)
* **FreeBSD**: [mutex(9)](https://www.freebsd.org/cgi/man.cgi?query=mutex&sektion=9)
* **Linux**: [Linux Labs — Kernel API — Mutex](https://linux-kernel-labs.github.io/refs/heads/master/labs/kernel_api.html#mutex)

Мьютекс должен в общем случае использоваться в каждой функции вашей файловой системы, являющейся точкой входа: это функции, которые вызывает VFS, а также фукнции, вызываемые механимзом планирования задач.

При размонтировании файловой системы обязательно следует отменить все запланированные задачи. Сделать это можно примерно следующим образом:

1. В структуре с личными данными файловой системы (`struct cwfs_mnt` по умолчанию) создать поле вида `volatile int cwm_shutdown_pending`. Оно должно выставляться в 1 в самом начале обработчика события размонтирования в коде вашей ФС, ещё до взятия блокировки, служа указанием для фоновых задач прекращать работу.

2. Ещё одно поле аналогичного типа (в Linux нужно будет использовать `atomic_t`) в этой же структуре пусть хранит количество запущенных задач. Оно понадобится для обратной связи — с его помощью запущенные задачи будут сообщать о своём завершении. Пусть оно будет называться `cwm_queued_cnt`. Оно должно увеличиваться при запуске на планирование новой задачи и уменьшаться, когда задача принудительно снимается с планирования, либо сама отказывается перепланировать себя. Чтобы избежать race condition, связанных с одновременной модификацией данного поля, следует использовать атомарные операции:

* **OpenBSD**: [atomic_dec_int(9)](https://man.openbsd.org/atomic_dec_int.9)
* **FreeBSD**: [atomic_substract_int(9)](https://www.freebsd.org/cgi/man.cgi?query=atomic_substract_int&sektion=9)
* **Linux**: [Linux labs — Kernel API — Atomic variables](https://linux-kernel-labs.github.io/refs/heads/master/labs/kernel_api.html#atomic-variables), см. `atomic_dec()`.

3. Попытаться в том коде размонтирования отменить запущенные по таймеру задачи (`timeout_del(9)` в *BSD, `del_timer(9)` в Linux). При успехе (т.е., если задача была успешно снята с планирования) `cwm_queued_cnt` должно уменьшаться с помощью тех же атомарных операций.

4. Так как возможна ситуация, что задача уже была запущена, то нужно дождаться её окончания. Самое простое — в цикле проверять значение `cwm_queued_cnt`, пока оно не станет равно 0; вот почему это поле должно быть `volatile` — иначе компилятор может решить, что все чтения дадут одинаковый результат и закеширует значение переменной в регистре ЦП, вместо того, чтобы загружать его каждый раз заново. Чтобы не тратить слишком много процессорного времени, между проверками можно предлагать ядру сменить поток выполнения:

* **OpenBSD**: [tsleep(9)](https://man.openbsd.org/tsleep.9)
* **FreeBSD**: [tsleep(9)](https://www.freebsd.org/cgi/man.cgi?query=tsleep&sektion=9)
* **Linux**: [Kernel Korner — Sleeping in the Kernel](https://www.linuxjournal.com/article/8144)

5. Теперь, когда все фоновые задачи закончили свою деятельность, можно даже не брать ту самую блокировку (если только вы не приспособили её ещё для чего-то, кроме синхронизации VFS с фоновыми задачами), а спокойно завершить свою работу.

В коде же запланированной задачи получается примерно такой расклад:
```c
	mutex_lock(&cwm->cwm_lock);

	// Этот блок можно вставлять/использовать несколько раз:
	// например, если наша функция создаёт много объектов в VFS,
	// то нет смысла заставлять ждать при размонтировании, когда
	// мы все эти объекты создадим.
	if (cwm->cwm_shutdown_pending) {
		atomic_dec(&cwm->cwm_queued_cnt);
		goto finish;
	}

	// ... полезная деятельность

	// Заново планируем свой запуск, снова убедившись,
	// что не надо останавливаться.
	if (cwm->cwm_shutdown_pending) {
		atomic_dec(&cwm->cwm_queued_cnt);
		goto finish;
	}
	mod_timer(cwm->cwm_timer, ...);   

finish:
	mutex_unlock(&cwm->cwm_lock);
```

`cwm_timer` — это собственно таймер, его тоже надо будет сохранить в `struct cwfs_mnt`.  Как и соответствующий workqueue, в которую будут отправляться запланированные задачи.
